/*
 * Copyright 2012 Marsrutai.lt
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package lt.marsrutai.mobile.android.service;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import lt.marsrutai.mobile.android.model.ArrivalTime;
import lt.marsrutai.mobile.android.util.Base64.InputStream;
import lt.marsrutai.mobile.android.util.Base64Utils;
import lt.marsrutai.mobile.android.util.StringUtils;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.os.Parcel;
import android.os.Parcelable;
import android.preference.PreferenceManager;
import android.util.Log;

public class SupportBookmarkService {
    
    private static final String TAG = "SupportBookmarkService";

    public static final String TRACK_BOOKMARK_KEY = "TRACK_BOOKMARK";
    public static final String STOP_BOOKMARK_KEY = "STOP_BOOKMARK";

    private Context context;
    private BookmarkService bookmarkService;

    public SupportBookmarkService(Context context) {
        this.context = context;
    }

    public <T> Set<T> getBookmarks(String storeKey) {

        String deflatedBookmarks = null;
        SharedPreferences sprefs = PreferenceManager.getDefaultSharedPreferences(context);
        try {
            deflatedBookmarks = sprefs.getString(storeKey, "");
        }
        catch (ClassCastException e) {
            e.printStackTrace();
        }

        Set<T> bookmarks = null;
        if ("".equals(deflatedBookmarks)) {
            bookmarks = new HashSet<T>();
        }
        else {
            bookmarks = Base64Utils.inflate(deflatedBookmarks);
        }

        return bookmarks;
    }

    public <T> void setBookmarks(Set<T> bookmarks, String storeKey) {
        String deflatedBookmarks = Base64Utils.deflate(bookmarks);

        SharedPreferences sprefs = PreferenceManager.getDefaultSharedPreferences(context);
        Editor editor = sprefs.edit();
        editor.putString(storeKey, deflatedBookmarks);
        editor.commit();
    }

    public void migrate() {
        // STOP BOOKMARKS
        try {
            Set<lt.marsrutai.mobile.android.model.TransportStopBookmark> bookmarks = createNewTransportStopBookmarks();
            int migratedCount = migrate(bookmarks);
            
            Log.i(TAG, String.format("Migrated %d/%d transport stop bookmarks", migratedCount, bookmarks.size()));
        }
        catch (Exception e) {
            Log.e(TAG, "Transport stop bookmark migration failed: " + e.getMessage());
        }
        finally {
            setBookmarks(Collections.emptySet(), STOP_BOOKMARK_KEY);
        }
        
        // TRACK BOOKMARKS
        try {
            Set<lt.marsrutai.mobile.android.model.TrackBookmark> bookmarks = createNewTrackBookmarks();
            int migratedCount = migrate(bookmarks);
            
            Log.i(TAG, String.format("Migrated %d/%d track bookmarks", migratedCount, bookmarks.size()));
        }
        catch (Exception e) {
            Log.e(TAG, "Track stop bookmark migration failed: " + e.getMessage());
        }
        finally {
            setBookmarks(Collections.emptySet(), TRACK_BOOKMARK_KEY);
        }
    }

    protected <T extends lt.marsrutai.mobile.android.model.Bookmark> int migrate(Set<T> newTransportStopBookmarks) {
        int migratedCount = 0;
        for (T bookmark : newTransportStopBookmarks) {
            boolean migrated = bookmarkService.add(bookmark);
            
            if (migrated) {
                migratedCount++;
            }
        }
        return migratedCount;
    }

    protected Set<lt.marsrutai.mobile.android.model.TransportStopBookmark> createNewTransportStopBookmarks() {
        Set<StopBookmark> stopBookmarks = getBookmarks(STOP_BOOKMARK_KEY);

        Set<lt.marsrutai.mobile.android.model.TransportStopBookmark> newTransportStopBookmarks = new HashSet<lt.marsrutai.mobile.android.model.TransportStopBookmark>();
        for (StopBookmark stopBookmark : stopBookmarks) {
            newTransportStopBookmarks.add(map(stopBookmark));
        }
        
        return newTransportStopBookmarks;
    }

    protected Set<lt.marsrutai.mobile.android.model.TrackBookmark> createNewTrackBookmarks() {
        Set<TrackBookmark> trackBookmarks = getBookmarks(TRACK_BOOKMARK_KEY);

        Set<lt.marsrutai.mobile.android.model.TrackBookmark> newTransportStopBookmarks = new HashSet<lt.marsrutai.mobile.android.model.TrackBookmark>();
        for (TrackBookmark trackBookmark : trackBookmarks) {
            newTransportStopBookmarks.add(map(trackBookmark));
        }
        
        return newTransportStopBookmarks;
    }

    private lt.marsrutai.mobile.android.model.TrackBookmark map(TrackBookmark trackBookmark) {
        lt.marsrutai.mobile.android.model.TrackBookmark newBokmark = new lt.marsrutai.mobile.android.model.TrackBookmark();
        newBokmark.setScheduleId(trackBookmark.getScheduleId());
        newBokmark.setScheduleName(trackBookmark.getScheduleName());
        newBokmark.setScheduleTrackName(trackBookmark.getScheduleTrackName());
        newBokmark.setScopeFolder(trackBookmark.getScope());
        newBokmark.setScopeName(trackBookmark.getScopeName());
        newBokmark.setTrackId(trackBookmark.getTrackId());
        newBokmark.setTrackName(trackBookmark.getTrackName());
        newBokmark.setTransportId(trackBookmark.getTransportId());
        
        return newBokmark;
    }

    private lt.marsrutai.mobile.android.model.TransportStopBookmark map(StopBookmark stopBookmark) {
        lt.marsrutai.mobile.android.model.TransportStopBookmark newBookmark = new lt.marsrutai.mobile.android.model.TransportStopBookmark();
        newBookmark.setScheduleId(stopBookmark.getScheduleId());
        newBookmark.setScheduleName(stopBookmark.getScheduleName());
        newBookmark.setScheduleTrackName(stopBookmark.getScheduleTrackName());
        newBookmark.setScopeFolder(stopBookmark.getScope());
        newBookmark.setScopeName(stopBookmark.getScopeName());
        newBookmark.setStopDirectionName(stopBookmark.getStopDirectionName());
        newBookmark.setStopId(stopBookmark.getStopId());
        newBookmark.setStopName(stopBookmark.getStopName());
        newBookmark.setTrackId(stopBookmark.getTrackId());
        newBookmark.setTrackName(stopBookmark.getTrackName());
        newBookmark.setTransportId(stopBookmark.getTransportId());
        return newBookmark;
    }

    public void setBookmarkService(BookmarkService bookmarkService) {
        this.bookmarkService = bookmarkService;
    }

    /**
     * Support ObjectInputStream required to load old bookmarks
     * 
     * @author Justas
     * 
     */
    public static class SupportObjectInputStream extends ObjectInputStream {

        public SupportObjectInputStream(InputStream in) throws IOException {
            super(in);
        }

        public SupportObjectInputStream(ByteArrayInputStream in) throws IOException {
            super(in);
        }

        @Override
        protected ObjectStreamClass readClassDescriptor() throws IOException, ClassNotFoundException {
            ObjectStreamClass resultClassDescriptor = super.readClassDescriptor();

            if (StringUtils.equals(resultClassDescriptor.getName(), "lt.marsrutai.mobile.android.model.StopBookmark")) {
                resultClassDescriptor = ObjectStreamClass.lookup(StopBookmark.class);
            }
            else if (StringUtils.equals(resultClassDescriptor.getName(), "lt.marsrutai.mobile.android.model.TrackBookmark")) {
                resultClassDescriptor = ObjectStreamClass.lookup(TrackBookmark.class);
            }

            return resultClassDescriptor;
        }
    }

    public static class StopBookmark implements Serializable, Parcelable {

        private static final long serialVersionUID = -7228378291856775463L;

        public static final Parcelable.Creator<StopBookmark> CREATOR = new Parcelable.Creator<StopBookmark>() {

            @Override
            public StopBookmark createFromParcel(Parcel source) {
                return new StopBookmark(source);
            }

            @Override
            public StopBookmark[] newArray(int size) {
                return new StopBookmark[size];
            }
        };

        private String scope;
        private String scopeName;

        private String stopId;
        private String stopName;
        private String stopDirectionName;

        private String transportId;

        private String scheduleId;
        private String scheduleName;
        private String scheduleTrackName;

        private String trackId;
        private String trackName;

        private List<ArrivalTime> arrivalTimes = new ArrayList<ArrivalTime>();

        public StopBookmark() {
        }

        public StopBookmark(Parcel in) {
            scope = in.readString();
            scopeName = in.readString();

            stopId = in.readString();
            stopName = in.readString();
            stopDirectionName = in.readString();

            transportId = in.readString();

            scheduleId = in.readString();
            scheduleName = in.readString();
            scheduleTrackName = in.readString();

            trackId = in.readString();
            trackName = in.readString();

            in.readList(arrivalTimes, ArrivalTime.class.getClassLoader());
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeString(scope);
            dest.writeString(scopeName);

            dest.writeString(stopId);
            dest.writeString(stopName);
            dest.writeString(stopDirectionName);

            dest.writeString(transportId);

            dest.writeString(scheduleId);
            dest.writeString(scheduleName);
            dest.writeString(scheduleTrackName);

            dest.writeString(trackId);
            dest.writeString(trackName);

            dest.writeList(arrivalTimes);
        }

        @Override
        public int describeContents() {
            return 0;
        }

        public String getScope() {
            return scope;
        }

        public void setScope(String scope) {
            this.scope = scope;
        }

        public String getStopId() {
            return stopId;
        }

        public void setStopId(String stopId) {
            this.stopId = stopId;
        }

        public String getTrackId() {
            return trackId;
        }

        public void setTrackId(String trackId) {
            this.trackId = trackId;
        }

        public List<ArrivalTime> getArrivalTimes() {
            return arrivalTimes;
        }

        public void setArrivalTimes(List<ArrivalTime> arrivalTimes) {
            this.arrivalTimes = arrivalTimes;
        }

        public String getScopeName() {
            return scopeName;
        }

        public void setScopeName(String scopeName) {
            this.scopeName = scopeName;
        }

        public String getTransportId() {
            return transportId;
        }

        public void setTransportId(String transportId) {
            this.transportId = transportId;
        }

        public String getScheduleId() {
            return scheduleId;
        }

        public void setScheduleId(String scheduleId) {
            this.scheduleId = scheduleId;
        }

        public String getScheduleName() {
            return scheduleName;
        }

        public void setScheduleName(String scheduleName) {
            this.scheduleName = scheduleName;
        }

        public String getScheduleTrackName() {
            return scheduleTrackName;
        }

        public void setScheduleTrackName(String scheduleTrackName) {
            this.scheduleTrackName = scheduleTrackName;
        }

        public String getTrackName() {
            return trackName;
        }

        public void setTrackName(String trackName) {
            this.trackName = trackName;
        }

        public String getStopName() {
            return stopName;
        }

        public void setStopName(String stopName) {
            this.stopName = stopName;
        }

        public String getStopDirectionName() {
            return stopDirectionName;
        }

        public void setStopDirectionName(String stopDirectionName) {
            this.stopDirectionName = stopDirectionName;
        }

        @Override
        public String toString() {
            return "StopBookmark [scope=" + scope + ", stopId=" + stopId + ", trackId=" + trackId + "]";
        }

        /**
         * Hash code is calculated only using scope, stopId and trackId because
         * BookmarkService must be able to update old bookmarks to new.
         */
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((scope == null) ? 0 : scope.hashCode());
            result = prime * result + ((stopId == null) ? 0 : stopId.hashCode());
            result = prime * result + ((trackId == null) ? 0 : trackId.hashCode());
            return result;
        }

        /**
         * Equals is calculated only using scope, stopId and trackId because
         * BookmarkService must be able to update old bookmarks to new.
         */
        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            StopBookmark other = (StopBookmark) obj;
            if (scope == null) {
                if (other.scope != null) {
                    return false;
                }
            }
            else if (!scope.equals(other.scope)) {
                return false;
            }
            if (stopId == null) {
                if (other.stopId != null) {
                    return false;
                }
            }
            else if (!stopId.equals(other.stopId)) {
                return false;
            }
            if (trackId == null) {
                if (other.trackId != null) {
                    return false;
                }
            }
            else if (!trackId.equals(other.trackId)) {
                return false;
            }
            return true;
        }

    }

    public static class TrackBookmark implements Serializable, Parcelable {

        private static final long serialVersionUID = -7228378291856775463L;

        public static final Parcelable.Creator<TrackBookmark> CREATOR = new Parcelable.Creator<TrackBookmark>() {

            @Override
            public TrackBookmark createFromParcel(Parcel source) {
                return new TrackBookmark(source);
            }

            @Override
            public TrackBookmark[] newArray(int size) {
                return new TrackBookmark[size];
            }
        };

        private String scope;
        private String scopeName;

        private String transportId;

        private String scheduleId;
        private String scheduleName;
        private String scheduleTrackName;

        private String trackId;
        private String trackName;

        public TrackBookmark() {
        }

        public TrackBookmark(Parcel in) {
            scope = in.readString();
            scopeName = in.readString();

            transportId = in.readString();

            scheduleId = in.readString();
            scheduleName = in.readString();
            scheduleTrackName = in.readString();

            trackId = in.readString();
            trackName = in.readString();
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeString(scope);
            dest.writeString(scopeName);

            dest.writeString(transportId);

            dest.writeString(scheduleId);
            dest.writeString(scheduleName);
            dest.writeString(scheduleTrackName);

            dest.writeString(trackId);
            dest.writeString(trackName);
        }

        @Override
        public int describeContents() {
            return 0;
        }

        public String getScope() {
            return scope;
        }

        public void setScope(String scope) {
            this.scope = scope;
        }

        public String getTrackId() {
            return trackId;
        }

        public void setTrackId(String trackId) {
            this.trackId = trackId;
        }

        public String getScopeName() {
            return scopeName;
        }

        public void setScopeName(String scopeName) {
            this.scopeName = scopeName;
        }

        public String getTransportId() {
            return transportId;
        }

        public void setTransportId(String transportId) {
            this.transportId = transportId;
        }

        public String getScheduleId() {
            return scheduleId;
        }

        public void setScheduleId(String scheduleId) {
            this.scheduleId = scheduleId;
        }

        public String getScheduleName() {
            return scheduleName;
        }

        public void setScheduleName(String scheduleName) {
            this.scheduleName = scheduleName;
        }

        public String getScheduleTrackName() {
            return scheduleTrackName;
        }

        public void setScheduleTrackName(String scheduleTrackName) {
            this.scheduleTrackName = scheduleTrackName;
        }

        public String getTrackName() {
            return trackName;
        }

        public void setTrackName(String trackName) {
            this.trackName = trackName;
        }

        @Override
        public String toString() {
            return "TrackBookmark [scope=" + scope + ", trackId=" + trackId + "]";
        }

        /**
         * Hash code is calculated only using scope and trackId because
         * BookmarkService must be able to update old bookmarks to new.
         */
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((scope == null) ? 0 : scope.hashCode());
            result = prime * result + ((trackId == null) ? 0 : trackId.hashCode());
            return result;
        }

        /**
         * Equals is calculated only using scope and trackId because
         * BookmarkService must be able to update old bookmarks to new.
         */
        @Override
        public boolean equals(Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            TrackBookmark other = (TrackBookmark) obj;
            if (scope == null) {
                if (other.scope != null) {
                    return false;
                }
            }
            else if (!scope.equals(other.scope)) {
                return false;
            }
            if (trackId == null) {
                if (other.trackId != null) {
                    return false;
                }
            }
            else if (!trackId.equals(other.trackId)) {
                return false;
            }
            return true;
        }

    }

}
